#!/bin/bash

# oradocs.sh 0.47 20100405
#
# oradocs.sh is an Oracle Documentation Library Manager.
#
# Copyright © 2010 Michael Roy <mclroy@gmail.com>
# License: GNU GPL version 3 or later <http://www.gnu.org/licenses/gpl.html>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# Oracle is a registered trademark of Oracle Corporation.
#

prog="oradocs.sh"
ODPROHOME="http://sites.google.com/site/mclroy/oradocs"

# Oracle Documentation
# http://www.oracle.com/technology/documentation/index.html
# This page contains links to the most current documentation for
# Oracle Database, Fusion Middleware, Developer Suite, Beehive and
# Applications/E-Business Suite.

# Three variables must be set for each library.
# The format of the names of this variables is:
# ORA_SRC|DOC|URL_LibraryID
# where the LibraryID identifies a configured library in the script.

# Oracle Database Documentation

# Here are a few special cases for the archaic libraries (DB 7, 8i, 9i)
ORA_SRC_DB7="http://www.oracle.com/technology/documentation/oracle7.html"
ORA_DOC_DB7="Oracle7 Server On-Line Generic Documentation CD-ROM Version 7.3.4"
ORA_URL_DB7="http://download.oracle.com/docs/cds/A57673_01.zip"
ORA_LST_DB7="DOC/products.htm"

ORA_SRC_DB8="http://www.oracle.com/technology/documentation/oracle8i.html"
ORA_DOC_DB8="Oracle8i On-Line Generic Documentation Release 3 (8.1.7)"
ORA_URL_DB8="http://download.oracle.com/docs/cds/A87860_01.zip"
ORA_LST_DB8="doc/products.htm"

ORA_SRC_DB9="http://www.oracle.com/technology/documentation/oracle9i.html"
ORA_DOC_DB9="Oracle9i Database Release 2 (9.2) Documentation Library"
ORA_URL_DB9="http://download.oracle.com/docs/cds/B10501_01.zip"
ORA_LST_DB9="nav/docindex.htm"

ORA_SRC_DB10="http://www.oracle.com/technology/documentation/database10gr2.html"
ORA_DOC_DB10="Oracle Database 10g Release 2 Documentation Library"
ORA_URL_DB10="http://download.oracle.com/docs/cds/B19306_01.zip"

ORA_SRC_DB111="http://www.oracle.com/technology/documentation/database11gr1.html"
ORA_DOC_DB111="Oracle Database 11g Release 1 Documentation Library"
ORA_URL_DB111="http://download.oracle.com/docs/cds/B28359_01.zip"

ORA_SRC_DB11="http://www.oracle.com/technology/documentation/database.html"
ORA_DOC_DB11="Oracle Database 11g Release 2 (11.2) Documentation"
ORA_URL_DB11="http://download.oracle.com/docs/cds/E11882_01.zip"

# Oracle Forms
# Oracle does not provide a library in the form of "CD" for Forms.
# Download PDFs from the main product page (ORA_SRC):

ORA_SRC_F6="http://www.oracle.com/technology/documentation/forms.html"
ORA_DOC_F6="Oracle Forms Developer and Forms Server Documentation"
ORA_URL_F6="" # no URL to download
ORA_DIR_F6="FORMS" # a special case
ORA_LST_F6="" # Technically, it is pdf/forms.html (from ORA_SRC)

ORA_SRC_F6i="http://www.oracle.com/technology/documentation/6i_forms.html"
ORA_DOC_F6i="Forms6i Documentation (Patch Release Notes)"
ORA_URL_F6i="" # no URL to download
ORA_DIR_F6i="FORMS6i" # a special case
ORA_LST_F6i="" # Technically, it is pdf/6i_forms.html (from ORA_SRC)


# Oracle Internet Application Server Documentation

ORA_SRC_AS8="http://www.oracle.com/technology/documentation/ias_arch_8i.html"
ORA_DOC_AS8="Oracle Internet Application Server 8i Release 1.0.1 Documentation"
ORA_URL_AS8="http://download.oracle.com/docs/cds/A97338_01.zip"

ORA_SRC_AS9="http://www.oracle.com/technology/documentation/ias.html"
ORA_DOC_AS9="Oracle9i Application Server Release 2 (9.0.3.0.0)"
ORA_URL_AS9="http://download.oracle.com/docs/cds/A97688_14.zip"

# Oracle® Fusion Middleware Documentation

ORA_SRC_F111="http://www.oracle.com/technology/documentation/middleware.html"
ORA_DOC_F111="Oracle® Fusion Middleware Online Documentation Library 11g Release 1 (11.1.1.1)"
ORA_URL_F111="http://download.oracle.com/docs/cds/E12839_01.zip"

ORA_SRC_F11="http://www.oracle.com/technology/documentation/middleware.html"
ORA_DOC_F11="Oracle® Fusion Middleware Online Documentation Library 11g Release 1 (11.1.1.2)"
ORA_URL_F11="http://download.oracle.com/docs/cds/E15523_01.zip"

# Oracle Developer Suite Documentation  

ORA_SRC_DS10="http://www.oracle.com/technology/documentation/devsuite.html"
ORA_DOC_DS10="Oracle Developer Suite 10g Release 2 (10.1.2) Documentation"
ORA_URL_DS10="http://download.oracle.com/docs/cds/B25016_08.zip"

# Oracle Applications Documentation  

ORA_SRC_EBS1211P="http://www.oracle.com/technology/documentation/applications.html"
ORA_DOC_EBS1211P="Oracle E-Business Suite Online Documentation Library Release 12.1.1+"
ORA_URL_EBS1211P="http://download.oracle.com/docs/cds/B53825_03.zip"
ORA_LST_EBS1211P="current/html/doclist.html"

ORA_SRC_EBS12="http://www.oracle.com/technology/documentation/applications.html"
ORA_DOC_EBS12="Oracle E-Business Suite Online Documentation Library Release 12.1.2"
ORA_URL_EBS12="http://download.oracle.com/docs/cds/B53825_04.zip"
ORA_LST_EBS12="current/html/doclist.html"

# Oracle® Enterprise Manager Documentation

ORA_SRC_EM10="http://www.oracle.com/technology/documentation/oem.html"
ORA_DOC_EM10="Oracle Enterprise Manager 10g Release 5 (10.2) Documentation"
ORA_URL_EM10="http://download.oracle.com/docs/cds/B16240_01.zip"
ORA_LST_EM10="doc/nav/portal_booklist.htm"
# Another too "progressive" library. It needs -C.

# Oracle Beehive Documentation

ORA_SRC_BH1="http://www.oracle.com/technology/documentation/beehive.html"
ORA_DOC_BH1="Oracle Beehive Release 1 (1.5)"
ORA_URL_BH1="http://download.oracle.com/docs/cds/E14897_01.zip"


function storevar {
  local line present
  [ -r "${ORADOCSCFG}" ] && while read line; do
    [[ "${line}" =~ "^(${1})=([\"']?)(.*)(\2)" ]] && { present="y"; break; }
  done  < ${ORADOCSCFG}
  [ ${#VERBOSE} -gt 2 ] && echo "Saving the ${1} variable \"${!1}\""
  if [ "${present}" ]; then
    [ "${BASH_REMATCH[3]}" != "${!1}" ] && sed -i "/^${1}/ s|.*|${1}=\"${!1}\"|" ${ORADOCSCFG}
  else
    echo -e "\n${1}=\"${!1}\"" >> ${ORADOCSCFG}
  fi
}

# Store oradocs.conf:

function store {
  local ora oradir doclib
  [ "$VERBOSE" ] && echo "Saving to the default config file: ${ORADOCSCFG}"
  eval "ORA_BKS_${DOCLIB}=\"${RUNBOOKS}\"" # keep the current list
  [ "$DRYRUN" ] || {
    if [ -f "${ORADOCSCFG}" ]; then # for existing config
      [ ! -w "${ORADOCSCFG}" ] && { echo "The default config file ${ORADOCSCFG} is not writable."; exit 1; }
      [ "$CHANGELIB" ] && storevar "DOCLIB"
      for ora in "ORA_"{"ZIP","REP","BKS"}"_${DOCLIB}"; do
        [ "${!ora}" ] && storevar "${ora}"
      done
    else # new config file
      [ "$VERBOSE" ] && echo "Creating the new config file: ${ORADOCSCFG}"
      {
        echo "#PDFVIEWER=\"${PDFVIEWER}\""
        echo "#RUNCOLS=${RUNCOLS}"
        echo "# Do not open more books than:"
        echo "#NOTMORETHAN=${NOTMORETHAN}"
        echo ""
        echo "# The default path to the main repository of libraries:"
        echo "DOCPATH=\"${DOCPATH}\""
        echo "# The default path to ZIP files may be set to a different directory from DOCPATH:"
        [ "${ZIPPATH}" = "${DOCPATH}" ] && echo -n "#"; echo "ZIPPATH=\"${ZIPPATH}\""
        echo "# The default library:"
        echo "DOCLIB=\"${DOCLIB}\""
        echo "# A utility to open a file or URL in the default application (browser):"
        echo "#XOPEN=\"${XOPEN}\""
        echo ""
        echo "# A library may be defined by the set of variables:"
        echo "# ORA_SRC_LIBID - the web page of the library, e.g. \"${ORA_SRC_DB11}\""
        echo "# ORA_DIR_LIBID - the top directory of the library in its ZIP file, it is not uniform though"
        echo "#                 it must be Oracle Part Number, e.g. \"${ORA_DIR_DB11}\""
        echo "#                 it is derived from ORA_URL_LIBID"
        echo "# ORA_DOC_LIBID - the title of the library"
        echo "# ORA_URL_LIBID - the URL of the ZIP file of the library"
        echo "# ORA_ZIP_LIBID - the local pathname of the ZIP file"
        echo "# ORA_REP_LIBID - the repository of the library"
        echo "# ORA_LST_LIBID - the HTML list of PDF books of the library"
        echo "# ORA_BKS_LIBID - the default list of book numbers of the list to be open"
        echo "# where LIBID is the library ID used through out of the ${prog} script, e.g. \"DB11\""
        echo "# Each library can be configured to have a different location separate from the main repository"
        echo "# as for the library itself as for its source ZIP file."
        echo ""
        for oradir in ${!ORA_DIR_*}; do
          doclib="${oradir#"ORA_DIR_"}"
          echo -e "\n# ${doclib}: Oracle Documentation Library ${!oradir}:"
          for ora in "ORA_"{"SRC","DIR","DOC","URL","ZIP","REP","LST","BKS"}"_${doclib}"; do
            echo "${ora}=\"${!ora}\""
          done
        done
      } > ${ORADOCSCFG}
    fi
  }
}

# Helper functions for the HTML parser:

function findline { while read line; do [[ "${line}" =~ "${1}" ]] && break; done; }
function findtable { findline "<table .*>"; }
function shortcuts { findline "<table .*(Shortcuts).*>"; } # It is not suitable for every library.
function nexttr { findline "<tr.*>"; }

function nexttd {
  [[ "${line}" =~ "<td.*>" ]] || findline "<((td)|(/?tr)).*>";
  [ "${BASH_REMATCH[3]}" ] && { td="tr"; return 8; } # Ooops, the end of row, no more td.
  td=${line//$'\r'};
  while [[ ! "${line}" =~ "</td>" ]] && read line; do
    [ "$line" ] && td+=" ${line//[$'\r\xa0']/ }"
  done
  [[ "${td}" =~ "((<td.*>)(.*)(</td>))(.*)" ]] && { td="${BASH_REMATCH[1]}"; line="${BASH_REMATCH[5]}"; }
}

function findtitle {
  local words="($sp*([-',_:&\(\+\*\.\)/[:alnum:]]*((</?i>)|(&reg;)|(&amp;)|(</?em$tagfill>))?)+)+"
  local -i wse=23 # the number of the "words" subexpression (se) in the next expression
  while nexttd; do
    [[ "$td" =~ "${tdpro}($sp*<($els)(($href)|($attr))*>)*(</a>)?$sp?(${words})${tdepi}" ]] && {
      book="${BASH_REMATCH[wse]}" # =(words)
      if [ "${BASH_REMATCH[wse+3]}" ]; then
         [ "${BASH_REMATCH[wse+4]}" ] && book="${book//<?(\/)i>}" # remove <i> and </i> tags
         [ "${BASH_REMATCH[wse+5]}" ] && book="${book//"&reg;"/®}"
         [ "${BASH_REMATCH[wse+6]}" ] && book="${book//"&amp;"/&}"
         [ "${BASH_REMATCH[wse+7]}" ] && book="${book//<?(\/)em*>}"
      fi
      [ -z "$1" ] && pdf="${BASH_REMATCH[fnamese]}" # maybe it is a book title with an HTML reference to its PDF
      return 0
    }
  done
}

function findbook {
  pdf=""; book=""; findtitle; [ "$td" = "tr" ] && return
  while [ "$pdf" ] || nexttd; do
    [[ "$pdf" || ( "$td" =~ "${tdpro}($sp*<($els)(($href)|($attr))*>)*(PDF|Contents)${tdepi}" ) ]] && {
      if [ "${BASH_REMATCH[httpse]}" ]; then # for the "http://..." (a fixed path)
        pdf="${locdir}${proddir}${BASH_REMATCH[fnamese]}.pdf" # the downloaded files must be in ./pdf directory
      else # for a file reference in the HREF
        # add an internal upper (loc|doc|current) and/or a product (server.817|acrobat) directory:
        if [ "${BASH_REMATCH[proddirse]}" ]; then # there is a product directory in the URL
          pdf="${locdir}${BASH_REMATCH[proddirse]}${BASH_REMATCH[fnamese]}.pdf"
        else # there is just a name
          pdf="${locdir}${proddir}${BASH_REMATCH[fnamese]}.pdf"
        fi
        [ "${BASH_REMATCH[tocse]}" ] && { [ -f "$pdf" ] || pdf=""; } # check pdf for "/toc.htm"
      fi
      if [ "$pdf" ]; then
        [ "${book}" -a ${#book} -lt 10 ] && { # Well, there is a title but it is suspiciously too short.
          local booktmp="${book}"
          findtitle "try" # again for another title
          [ ${#booktmp} -ge ${#book} ] && book="${booktmp}" # restore
        }
        { ((++i)); TITLE[i]="$book"; BOOK[i]="$pdf"; }
      fi
      return 0
    };
    pdf="" # 
  done
}

# Process a stream of HTML data for rows of books (titles and references of PDF files)
function loadstream {
  local line td # collect tds from lines
  local sp="[[:space:]]"
  local els="(a)|span|b|dd|small|font"
  local attrval="[-\+,#[:alnum:]%]*"
  local attr="$sp+([[:alpha:]]+)(=(\".*\"|'.*'|${attrval}))?"
  local tagfill="($sp*[^>]*)*"
  local tdpro="<td$tagfill>" tdepi="($sp*</($els)>)*($sp*)</td>" #-logues
  local fpw="[\.[:alnum:]]+" # a file path "word"
  local urlre="$sp*(((http://)($fpw/){2})|(\.*/))?($fpw/)?([_[:alnum:]]+)((\.pdf)|(/toc\.htm))" # (10)
  local href="$sp+href=([\"'])${urlre}\7" # (9) # Sorry for the back reference.
  local -i httpse=10 proddirse=13 fnamese=14 tocse=17
  findtable # Well, this line is just for fun. There are a lot of tables usually.
  while [ "$td" = "tr" ] || nexttr && [ "$line" ]; do
    findbook
  done
}

# Take a title from the first page of a PDF file
function owntitle { pdftotext -q -l 1 -nopgbrk "$1" - | sed -e 's/\(\.*\)//' -e '/^$/d' | head -q -n 4 | tr '\n' ' '; }

# The HTML parser to load a list of books from RUNLST ("nav/portal_booklist.htm")
# Apparently, uniformity is of Oracle's least concern for the lists.
# Well, they are at least in HTML, almost all of them.

function loadlist {
  local -i i=0
  local locdir proddir product list book pdf
  local listre="(((.*/)?)((.*)/))(((products|doc|toc|list|platdocs|.*forms|oracle.*|index)+)\.htm(l?))" # no leading "../"
  [ ${#VERBOSE} -gt 1 ] && { echo -e "\nLoading the list of books from: ${RUNPATH}/${RUNLST}"; }
  [[ "$RUNLST" =~ "$listre" ]] && {
    locdir="${BASH_REMATCH[3]}"; proddir="${BASH_REMATCH[4]}"; product="${BASH_REMATCH[5]}"
  }
  [ -r "${RUNLST}" ] && loadstream < ${RUNLST}
  [ $i -eq 0 ] && {
    # For archaic libraries, namely for 8i, a quite buggy library, or for too progressive ones quite buggy also
    for list in $(find -maxdepth 3 -regextype posix-extended -iregex $listre -printf "%P "); do
      [[ "$list" =~ "$listre" ]] && { 
        locdir="${BASH_REMATCH[3]}"; proddir="${BASH_REMATCH[4]}"; product="${BASH_REMATCH[5]}"
        pdf="${locdir}${proddir}index.pdf"
        [ -f "$pdf" ] && { book="Index of $product: $(owntitle "$pdf")"; ((++i)); TITLE[i]="$book"; BOOK[i]="$pdf"; }
        [ -r "${list}" ] && loadstream < ${list}
      }
    done
  }
}

# Fit a title in the screen
function formattitle { # 1 - title, 2 - used part of line (in characters).
  if [[ RUNCOLS -gt 0 && RUNCOLS -gt $2 && ${#1} -gt RUNCOLS-$2 ]]; then
    printf "%s..." "${1:0:RUNCOLS-3-$2}" # the "3" for the dots "...".
  else # as it is
    printf "%s" "${1}"
  fi
}

function printtitle { # of a book by its number
  local -i i=$1; local title
  [[ i -gt 0  &&  i -le ${#BOOK[*]} ]] && {
    [ "$OWNTITLES" ] && title="$(owntitle "${BOOK[i]}")" || title="${TITLE[i]}"
    formattitle "${title}" $2
  }
}

function listbooks {
  local -i i=0 length=${MAXLEN:-0} from=${1:-0} size=${#BOOK[*]} # well, arrays are zero-bazed
  local path=""; [ ${#LISTBOOKS} -gt 1 ] && path="file://${RUNPATH}/" # for the "-ll" option
  [ "${VERBOSE}" ] && echo "${RUNDOC}"
  [ ${#VERBOSE} -gt 2 ] && echo "The number of columns of the terminal: ${RUNCOLS}"
  if [[ length -eq 0 ]]; then # calculate it
    for ((i=1; i<=size; ++i)); do # the array is used from index 1 and the size is the number of elements
      [ $length -le ${#BOOK[i]} ] && length=${#BOOK[i]}+1 # "index" workaround: let it be a bit longer
    done;
    [[ length -gt 0 ]] && { length+=${#path}; MAXLEN=$length; } # keep it for next calls
  fi
  for ((i=from+1; i<=size; ++i)); do
    printf "%4i: %${length}s  %s\n" $i "${path}${BOOK[i]}" "$(printtitle $i $((length+8)))";
  done;
  echo ""
}

function checkbooks {
  local -i keep=${#BOOK[*]}; local -i i=$keep j match; local book pdf
  local path=""; [ ${#LISTBOOKS} -gt 1 ] && path="file://${RUNPATH}/"
  local -i pl=${#path}
  [ "${VERBOSE}" ] && {
    echo "Checking the current library for lost books..."
    echo "The total number of PDFs in the current library is: $(find -iname \*.pdf | wc -l)"
  }
  for pdf in $(find -iname \*.pdf -printf "%P "); do
    for ((j=1, match=1; j<=keep; ++j)); do
      [ "$pdf" = "${BOOK[j]}" ] && { match=0; break; }
    done;
    [ $match -eq 0 ] && continue || { # add a lost book
      book="$(owntitle "$pdf")"
      { ((++i)); TITLE[i]="$book"; BOOK[i]="$pdf"; }
      [[ $MAXLEN -lt pl+${#BOOK[i]} ]] && unset MAXLEN # recount it in listbooks
    }
  done
  [ $i -gt $keep ] && {
    echo "The lost books:"
    listbooks $keep
  } || echo -e "No book is lost.\n"
}

function checkpath {
  if [ -d "$1" ]; then
    [ "$2" = "w" ] && { [ -w "$1" ] || { echo "${BASENAME}: Unable to write to: $1" >&2; return 2; } }
    [ "$(pwd)" = "$1" ] && return 0
    [ ${#VERBOSE} -gt 1 ] && echo "Changing the current directory to: $1"; cd "$1"
  else
    echo "${BASENAME}: No such directory: $1" >&2; return 1
  fi
}

function ask {
  case "${DEFAULTANSWER}" in
    "y") { echo "${1} [Y]"; return 0; } ;;
    "n") { echo "${1} [N]"; return 1; } ;;
  esac
  read -n 2 -p "${1} [y/N] " ANSWER;
  [ "${ANSWER:-"n"}" = "y" ]
}

function parseurl { sed -n -e "/http.*\.pdf/ { s/^.*\(http.*\.pdf\).*$/$1\1/p }"; }

# Oracle provides some libraries in the form of a list of PDFs on the ORA_SRC page.
# Download such archaic library into a "./pdf" directory under its ORA_PATH:

function download7 {
  [ "${RUNSRC}" ] || { echo "No source (ORA_SRC_*) is configured for the current library."; exit; }
  checkpath "${RUNREP}" "w" || exit # It is RUNREP anyway. It is set to DOCPATH or to its own value.
  echo -e "\n${RUNDOC}"
  ask "Are you sure to download the special library from ${RUNSRC} into ${RUNPATH}?" || exit
  mkdir -p "${RUNPATH}/pdf" || exit
  checkpath "${RUNPATH}" "w" || exit
  echo -e "${BASENAME}: Downloading PDFs into ${RUNPATH}/pdf\n"
  [ "$DRYRUN" ] || case "$WEBTOOL" in
    (wget) wget -P pdf "${RUNSRC}"
           wget -O - "${RUNSRC}" | parseurl | wget -c -P pdf -i -
           ;;
    (curl) checkpath "${RUNPATH}/pdf" "w" || exit
           curl -q -O -R "${RUNSRC}"
           curl -q "${RUNSRC}" | parseurl "--url " | curl -q -R -C - --remote-name-all --config -
           ;;
    (*)    $WEBTOOL "${RUNSRC}" ;; # Whatever way one may wish to.
  esac || exit
}

# Download a library in the form of a zipped CD of the library:

function download {
  [ "${RUNZIP}" ] || { echo "No ZIP file is configured for the current library."; exit; }
  echo -e "\n${RUNDOC}"
  if [[ "${RUNZIP}" =~ "((/[^/]+)+)/(([[:alnum:]_]*)(\.zip))" ]]; then
    checkpath "${BASH_REMATCH[1]}" "w" || exit
    ask "Are you sure to download ${RUNURL} into ${BASH_REMATCH[1]}?" || exit
  else
    echo "The ZIP file is misconfigured: ${RUNZIP}"; exit
  fi
  echo -e "${BASENAME}: Downloading ${RUNURL}\n"
  [ "$DRYRUN" ] || case "$WEBTOOL" in
    (wget) wget -c "${RUNURL}" ;;
    (curl) curl -R -C - -O "${RUNURL}" ;;
    (*)    $WEBTOOL "${RUNURL}" ;;
  esac || exit
  unset WEBTOOL
}

function install {
  checkpath "${RUNREP}" "w" || exit
  [ "${RUNZIP}" ] || { echo "No ZIP file is configured for the current library."; exit; }
  ask "Are you sure to unzip ${RUNZIP}?" || exit
  echo -e "${BASENAME}: Unzipping ${RUNZIP}\n"
  [ -f "${RUNZIP}" ] || { echo "No such ZIP file: ${RUNZIP}"; exit 1; }
  [ "$DRYRUN" ] || {
    unzip -l "${RUNZIP}" "${RUNDIR}/" >/dev/null
    if [ $? -ne 0 ]; then
      mkdir "$RUNPATH"
      checkpath "$RUNPATH" "w" || exit
      echo -e "${BASENAME}: Unzipping into: $RUNPATH"
    fi
    unzip "${RUNZIP}" || exit
  }
  unset INSTALL
}

function usage {
  [ "$1" ] && { [[ ! "$1" =~ "[[:space:]]" ]] && local uo="unrecognized option: "; echo "${BASENAME}: ${uo}$1"; }
  echo "Usage: ${BASENAME} [-v]* [-DOCLIB] [-y] [-H] [-w|--wget|--curl|--web] [-A] [-i]" \
       "[-L] [-l]* [-O] [-o] [-d] [-C] [-T] [-V|--version] [-h|--help] [-a] [-s] [[-t] RE]* [[-n] number]*"
  exit 1
}

function help {
  echo "Usage: ${prog} [option]"
  echo "Options:"
  echo "  -DOCLIB   set the current library to the DOCLIB, for example, DB11 (see the -L option)"
  echo "  --wget    download the library with wget"
  echo "  --curl    download the library with curl"
  echo "  -w|--web  download the library with a utility set by the WEBTOOL variable (wget by default)"
  echo "  -A  try to download the library as an archaic one"
  echo "  -i  install the library from its ZIP file"
  echo "  -L  list configured libraries"
  echo "  -l  list books of the current library"
  echo "  -l  double -l option is for a long listing format; -ll is acceptable"
  echo "  -o  open the list of books of the current library in a browser"
  echo "  -O  open the source page of the current library in a browser"
  echo "  -H  open the home page of the ${prog} project in a browser"
  echo "  -d  dry run"
  echo "  -C  check the current library for lost books"
  echo "  -T  take book titles from PDFs (a very slow operation)"
  echo "  -s  store settings (the current library ID and the run list of books)"
  echo "  -y  assume an answer of Yes to all questions"
  echo "  -v  turn on verbose output; multiple -v options make it more verbose; -vv|-vvv are acceptable"
  echo "  -t REs        search through the titles of books with REs (regular expressions)"
  echo "  -n numbers    search books by numbers"
  echo "  -a            append the books to the run list of the current library"
  echo "  -V|--version  display the version of ${prog}"
  echo "  -h|--help     print this help"
  echo "Report bugs to: \"Michael Roy\" <mclroy@gmail.com>"
}

function version {
  while read line; do
    [[ "${line}" =~ "^# (((${prog} ([[:digit:]]+))|(Copyright)|(License))(.*))" ]] && echo "${BASH_REMATCH[1]}"
  done < $0
}

function booknumbers {
  for booknumber; do
    [ "$booknumber" ] && [[ "$booknumber" =~ "^([[:digit:]]+)$" ]] && [ $booknumber -le ${#TITLE[*]} ] \
      && { RUNBOOKS="${RUNBOOKS:+"$RUNBOOKS "}$booknumber"; } \
      || { [ "$VERBOSE" ] && echo -e "No book was found for: $booknumber" \
           "\nCheck the list of the library with the -l option."; }
  done
}

function booktitles {
  local -i cnt=0
  local found="Found: "
  for s in "${!SEARCHSTRINGS[@]}"; do
    for i in ${!TITLE[*]}; do
      if [[ "${TITLE[i]}" =~ "${SEARCHSTRINGS[s]}" ]]; then
        RUNBOOKS="${RUNBOOKS:+"$RUNBOOKS "}${i}"; ((cnt++))
        [ ${#VERBOSE} -gt 1 ] && { echo "${found}$(printtitle $i ${#found})"; }
      fi
    done
    [ $cnt -eq 0 ] && { [ "$VERBOSE" ] && echo "No match was found for: ${SEARCHSTRINGS[s]}"; }
  done
}

#
# The start of the block of code for sorting the RUNBOOKS list
#
# quicksort functions: swap and part

function swap {
  local val=${bookarray[$1]}
  bookarray[$1]=${bookarray[$2]}
  bookarray[$2]=$val
}

function part {
  local left=$1
  local pval=${bookarray[$pivot]}
  swap $pivot $2
  for ((i=$1; i<$2; i++)); do
    [ ${bookarray[i]} -le $pval ] && swap $i $((left++));
  done
  swap $left $2
  pivot=$left
}

function quicksort { # $1 - left; $2 - right
  local -i pivot
  if [ $1 -lt $2 ]; then
    pivot=$((($1+$2)/2))
    part $1 $2 # sets pivot
    quicksort $1 $((pivot-1))
    quicksort $((pivot+1)) $2
  fi
}

function compress {
  local -i i p k size=${#bookarray[*]}
  for ((i=0, p=0; i<size; i++)); do # remove repeating numbers
    [ $p -lt $i ] && { [ ${bookarray[p]} -eq ${bookarray[i]} ] && unset bookarray[i] || p=$i; }
  done
  for ((i=0; i<size; i++)); do
    [ -z "${bookarray[i]}" ] && continue
    for ((k=i+1; k<size; k++)); do
      [ "${bookarray[k]}" ] && { [ "${BOOK[${bookarray[k]}]}" = "${BOOK[${bookarray[i]}]}" ] && unset bookarray[k]; }
    done
  done
}

function sortbooks {
  local -a bookarray; bookarray+=($RUNBOOKS)
  quicksort 0 $((${#bookarray[*]}-1))
  compress # bookarray
  RUNBOOKS="${bookarray[@]}"
}

#
# The end of the block of code for sorting the RUNBOOKS list
#

function runpdf {
  local -i cnt=0
  local books
  [ "$VERBOSE" ] && echo "${RUNDOC}"
  for i in $RUNBOOKS; do
    [ -f "${BOOK[i]}" ] && {
      books="${books:+"${books} "}${BOOK[i]}"; ((cnt++))
      [ "$VERBOSE" ] && printf "%4i: %s\n" $i "$(printtitle $i 6)"
    }
  done;
  [ ${cnt} -gt $NOTMORETHAN ] && {
    [ "$VERBOSE" ] && echo "${cnt} books were found."
    ask "Are you sure to open all ${cnt} books?" || exit
    echo -e "${BASENAME}: Running ${cnt} books...\n";
  }
  [ ${cnt} -gt 0 ] && {
    [ ${#VERBOSE} -gt 2 ] && { echo "Running: $books"; }
    [ "$DRYRUN" ] || $PDFVIEWER $books >/dev/null 2>&1 &
  }
}

# ORA_URL* -> ORA_ZIP* -> ORA_DIR* are derived
function setvars {
  [ "$LISTLIBS" ] && echo "Oracle Documentation Libraries"
  for url in ${!ORA_URL_*}; do # ORA_URLs must be set even empty
    local doclib="${url#"ORA_URL_"}" # a Library ID
    local libdir="ORA_DIR_$doclib" # an Oracle Part Number & a library directory
    local libtitle="ORA_DOC_$doclib" # a title
    local libzip="ORA_ZIP_$doclib" # a ZIP file
    local librep="ORA_REP_$doclib" # a path to the repository of the library
    local liblist="ORA_LST_$doclib" # a list of documents of a library
    if [[ "${!url}" =~ "http://(.*/)*(.*)+\.zip" ]]; then # (1): a path to (2): Part Number product
      eval ${libdir}="${BASH_REMATCH[2]}"
      [ -z "${!libzip}" ]   && eval "${libzip}=\"${ZIPPATH}/${BASH_REMATCH[2]}.zip\""
      [ -z "${!librep}" ]   && eval "${librep}=\"${DOCPATH}\"" # DOCPATH
      [ -z "${!liblist}" ]  && eval "${liblist}=\"nav/portal_booklist.htm\""
      [ -z "${!libtitle}" ] && eval "${libtitle}=\"${BASH_REMATCH[2]} Documentation Library\""
    elif [ -z "${!url}" -a "${!libdir}" ]; then # The variables are defined anyway.
      [ -z "${!libzip}" ]   && eval "${libzip}=" # Well, it should be empty. No URL no ZIP.
      [ -z "${!librep}" ]   && eval "${librep}=\"${DOCPATH}\"" # DOCPATH
      [ -z "${!liblist}" ]  && eval "${liblist}=" # Technically, the same reason as for libzip.
      [ -z "${!libtitle}" ] && eval "${libtitle}=\"${!libdir} Documentation Library\""
    else
      usage "A misconfiguration detected." # problem is met
    fi
    [ "$LISTLIBS" ] && {
      local present="n/a" listpath="${!liblist:+"${!librep}/${!libdir}/${!liblist}"}"
      [ "${listpath}" ] && { [ -f "${listpath}" ] && present="Installed" || present="Not installed"; } || present="n/a"
      if [ -z "$VERBOSE" ]; then
        printf "%-8s%15s (%-9s)  %s\n" "${doclib}" "${present}" "${!libdir}" "$(formattitle "${!libtitle}" 37)"
      else
        printf "\n%-11s %s\n" "$doclib" "${present}"
          printf "  %-9s %s\n" "Part No.:" "${!libdir}"
          printf "  %-9s %s\n" "Title:" "$(formattitle "${!libtitle}" 12)"
          printf "  %-9s %s\n" "Path:" "${!librep}/${!libdir}"
        if [ ${#VERBOSE} -gt 1 ]; then
          printf "  %-9s %s  %s\n" "List:" "${listpath:-"<empty>"}" "[${present}]"
          [ "${!libzip}" ] && { [ -f "${!libzip}" ] && present="present" || present="missing"; } || present="n/a"
          printf "  %-9s %s  %s\n" "Zip:" "${!libzip:-"<empty>"}" "[${present}]"
        else
          [ "${!libzip}" -a -f "${!libzip}" ] && printf "  %-9s %s\n" "Zip:" "${!libzip}"
        fi
      fi
    }
  done
}

# Convert the internal ID to LibraryID if it is set as Oracle Part Number (ORA_DIR_*)
function convertlibid {
  for libdir in ${!ORA_DIR_*}; do # sorry for doclib and libdir
    [ "${!libdir}" = "${doclib}" ] && { doclib="${libdir#"ORA_DIR_"}"; return; }
  done
  return 1
}

# The current library may be set to a "default" library at a start or in a config,
# or to some library in the command line.
# It may be set by LibraryID or by Oracle Part Number.
# Check the ID, convert it to LibraryID, and set the default library:

function checkchangelib {
  local doclib="${1:-$DOCLIB}" msg="${1:+"Changing the library"}"
  local libdir="ORA_DIR_${doclib}"
  [ -z "$1" -a "${!libdir}" ] && return # ok if just checking DOCLIB else try to check or convert
  if [ "${!libdir}" ] || convertlibid; then
    [ "$1" ] && [ "$DOCLIB" = "${doclib}" -o "$DOCLIB" = "${!libdir}" ] && msg="${1:+"The library is already set"}"
    DOCLIB="$doclib"
    [ "$VERBOSE" ] && echo -e "\n${msg:-"Setting the library"} to: $DOCLIB (${!libdir})"
  else
    usage "No such library: ${doclib}"
  fi
}

# Set runtime variables and load the current library
function setlib {
  local ora var runvar
  checkchangelib # for DOCLIB set in a config
  [ "$VERBOSE" ] && echo -e "\n  Default config file: ${ORADOCSCFG}" \
    "\n  Last config file: ${LASTCFG:-"no config file loaded"}" \
    "\n  PDF viewer: ${PDFVIEWER}" \
    "\n  Active Documentation library: ${DOCLIB}"
  [ ${#VERBOSE} -gt 3 ] && for ora in "ORA_"{"SRC","URL","REP","DIR","DOC","ZIP","LST","BKS"}"_${DOCLIB}"; do
    echo "  $ora: ${!ora:-"<empty list>"}";
  done
  for var in "SRC" "URL" "REP" "DIR" "DOC" "ZIP" "LST" "BKS"; do
    runvar="RUN${var}"; ora="ORA_${var}_${DOCLIB}"
    eval ${runvar}='"${!ora}"'
    [ ${#VERBOSE} -gt 2 ] && echo "  ${runvar}: ${!runvar:-"<empty>"}"
  done
  [ "$RUNBKS" ] && RUNBOOKS="${RUNBKS}"
  [ "$RUNREP" -a "$RUNDIR" ] && RUNPATH="${RUNREP}/${RUNDIR}"
  [ "$VERBOSE" ] && echo -e "  Path to the library: ${RUNPATH}"
  [ "$STORE" ] && store # current DOCLIB
  [ "$WEBTOOL" -o "$INSTALL" ] && return
  checkpath "${RUNPATH}" || exit
  loadlist
  [ "$VERBOSE" ] && echo -e "  Number of books in the library: ${#TITLE[*]}\n"
}

function loadconfig {
  if [ "$1" ]; then
    [ ${#VERBOSE} -gt 2 ] && echo "Probing the config file: $1";
    [ -f "$1" ] && { [ ${#VERBOSE} -gt 1 ] && echo "Loading the config file: $1"; . "$1"; LASTCFG="$1"; }
  fi
}

function openhome { [ "$DRYRUN" ] || { "$XOPEN" "${ODPROHOME}"; } }

function opensource {
  [ "${VERBOSE}" ] && echo "${RUNDOC}";
  [ "${VERBOSE}" ] && echo "${RUNSRC}";
  [ "$DRYRUN" ] || "$XOPEN" "${RUNSRC}"
}

function openlist {
  [ "${VERBOSE}" ] && echo "${RUNDOC}"
  [ "${VERBOSE}" ] && echo "${RUNPATH}/${RUNLST}"
  [ "$DRYRUN" ] || {
    if [ "${RUNLST}" ]; then
      "$XOPEN" "${RUNPATH}/${RUNLST}" # the common way
    else
      [ "${VERBOSE}" ] && echo "${RUNSRC}";
      "$XOPEN" "${RUNSRC}" # for an archaic library without RUNLST
    fi
  }
}

function addbooknumber { [[ "$1" =~ "^([[:digit:]]+)$" ]] && BOOKNUMBERS="${BOOKNUMBERS:+"$BOOKNUMBERS "}${1}"; }

#
# ##########
# Start here
#

declare -a SEARCHSTRINGS TITLE BOOK
declare BOOKNUMBERS

# BASENAME
[[ "$0" =~ "(/.*)*/(.*)(\..*)" ]] || [[ "$0" =~ "(/.*)*/(.*)" ]] && {
  : ${DIRNAME:="${BASH_REMATCH[1]}"}
  : ${BASENAME:="${BASH_REMATCH[2]}${BASH_REMATCH[3]}"}
  : ${CFGFILE:="${BASH_REMATCH[2]}.conf"}
}

# Parse parameters, options and arguments
for param; do
  case "${param}" in
    (--init) INIT=y; STORE=y; VERBOSE+=y ;;
    (--curl|--wget)   : ${WEBTOOL:="${param:2}"} ;; # download
    (-w|--web)        : ${WEBTOOL:="wget"} ;;
    (-A) ARCHAIC=y;   : ${WEBTOOL:="wget"} ;;
    (-i) INSTALL=y ;;
    (-o) OPENLIST=y ;;
    (-O) OPENSRC=y ;;
    (-H) OPENHOME=y ;;
    (-v) VERBOSE+=y ;;
    (-vv) VERBOSE+=yy ;;
    (-vvv) VERBOSE+=yyy ;;
    (-s) STORE=y ;;
    (-a) APPEND=y ;;
    (-d) DRYRUN=y ;;
    (-y) DEFAULTANSWER=y ;;
    (-l) LISTBOOKS+=y ;;
    (-ll) LISTBOOKS+=yy ;;
    (-L) LISTLIBS=y ;;
    (-C) CHECKBOOKS=y ;; # when the $prog is unable to retrieve the list of books, check PDFs directly
    (-T) OWNTITLES=y ;; # get titles from PDFs directly
    (-h|--help) HELP=y ;;
    (-V|--version) SHOWVERSION=y ;;
    (-t|-n) bookmode=${param:1} ;;
    (*)
      if [[ ! "${param}" =~ "^-(.+)$" ]]; then # arguments
        case "$bookmode" in
          (t) SEARCHSTRINGS+=("${param}") ;;
          (n) addbooknumber "${param}" || usage "invalid operand: ${param}" ;;
          (*) addbooknumber "${param}" || SEARCHSTRINGS+=("${param}") ;;
        esac
      elif [[ "${param}" =~ "^-([[:alpha:]]+[[:digit:]]+(_?[[:alnum:]]+)?|[[:alpha:]]{3,})$" ]]; then # DOCLIB
        if [ -z "$CHANGELIB" ]; then
          CHANGELIB="${BASH_REMATCH[1]}"
        else
          usage "A library may be changed only once. (${BASH_REMATCH[1]})"
        fi
      else # confusion
        usage "${param}";
      fi
      ;;
  esac
done

[ "$SHOWVERSION" ] && { version; exit 0; }
[ "$HELP" ] && { help; exit 0; }
[ "$LISTBOOKS" -o "$LISTLIBS" -o "$WEBTOOL" -o "$INSTALL" ] && EXPLICITACTION=y;
[ "$BASENAME" ] && [ "$VERBOSE" -o "$EXPLICITACTION" ] && echo "${BASENAME}:"

# Load configuration file(s) (CFGFILE)
: ${ORADOCSCFG:=~/.${CFGFILE}} # the default config is "~/.oradocs.conf"
[ "$CFGFILE" ] && for config in {/etc/,${DIRNAME}/,~/}${CFGFILE} ${ORADOCSCFG}; do
  loadconfig "${config}"
done

# Default values
: ${PDFVIEWER:="evince"}
: ${RUNCOLS:=${COLUMNS:-$(tput cols)}}
: ${XOPEN:="xdg-open"} # an external utility to open URLs in a browser
: ${NOTMORETHAN:=7} # the maximum number of books allowed to be open
: ${DOCPATH:="/usr/oradocs"}
: ${ZIPPATH:="${DOCPATH}"}
: ${DOCLIB:="DB11"} # the default Documentation library

setvars # ORA_URL_* -> ORA_ZIP_* -> ORA_DIR_* are derived (with their relative ORA_PATH_* and ORA_LST_*)
[ "$LISTLIBS" ] && exit 0
[ "$CHANGELIB" ] && checkchangelib ${CHANGELIB} # change the current library
[ "$INIT" ] && { store; exit 0; } # says: Saving to the default config file
shopt -s nocasematch # bash matches patterns in a case-insensitive fashion
shopt -s extglob # the extended pattern matching as in pathname expansion
setlib # set the current library and the RUN* run-time variables according to the library

[ "$DRYRUN" ] && DEFAULTANSWER="n"
[ "$WEBTOOL" ] && { if [ "$ARCHAIC" ]; then download7; else download; fi; }
[ "$INSTALL" ] && install && setlib
[ "$LISTBOOKS" ] && listbooks # just list them
[ "$CHECKBOOKS" ] && checkbooks # check PDF files
[ "$OPENSRC" ] && opensource # in a browser
[ "$OPENLIST" ] && openlist # in a browser
[ "$OPENHOME" ] && openhome # in a browser

[ "$APPEND" ] || { [ "$BOOKNUMBERS" -o ${#SEARCHSTRINGS[*]} -gt 0 ] && unset RUNBOOKS; }
booknumbers $BOOKNUMBERS   # append to RUNBOOKS
booktitles  #SEARCHSTRINGS # append to RUNBOOKS
if [ "$RUNBOOKS" ]; then
  sortbooks # sort RUNBOOKS
  runpdf # for the books of the RUNBOOKS list
elif ! [ "$OPENLIST" -o "$OPENSRC" -o "$OPENHOME" -o "$EXPLICITACTION" ]; then
  usage "No books to open."
fi
[ "$STORE" ] && store # save the current settings

exit 0

# Mon Apr 05 2010 Michael Roy <mclroy@gmail.com> - 0.47
# minor bugs

# Wed Jan 27 2010 Michael Roy <mclroy@gmail.com> - 0.45
# initial public release
